feat(linux): fcitx5 候选框听写状态提示 + AppImage 插件自动安装 + 热键启动容错#520
Conversation
- fcitx5 插件新增 SetAuxDown/ClearAuxDown DBus 接口,在候选词列表下方 显示听写状态文字(收音中→识别中→润色中→已插入),切窗口自动跟随焦点 - 优先使用当前焦点 IC 展示 auxDown,失焦 IC 降级兜底,避免面板不渲染 - flushUI 排空旧事件后再设 auxDown,防止按键事件竞态覆盖状态文字 - start_dictation_signal_listener 启动时等待 fcitx5 最多 30s,监听 NameOwnerChanged 在 fcitx5 重启后自动重新同步快捷键绑定 - AppImage 打包 libopenless.so 为资源,启动时检测缺失自动安装到 ~/.local/lib/fcitx5/ 和 ~/.local/share/fcitx5/addon/ - Linux 胶囊窗口不在 Wayland/X11 显示,状态完全走 fcitx5 输入面板 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PR Reviewer Guide 🔍(Review updated until commit 2832437)Here are some key observations to aid the review process:
|
Linux 已从 NoActivate 列表移除(走 fcitx5 auxDown 状态提示),测试断言 需同步更新为 FallbackShow。 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
两个根因: 1. end_time 判断用 is_some() 而非 > 0:API 对 interim 结果设 end_time:0, 导致所有中间结果被当成 final 推入 segments 2. 累积文本拼接:同一句 API 多次发送("你"→"你好"→"你好吗"), 每次作为新 segment push,join 后变成重复拼接 修复: - end_time 改用 > 0 判断真正的句子结束 - 引入 sentence_id → BTreeMap 按序存储,同一 sentence_id 后到覆盖前到 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit 7635df5 |
1. clearAuxDown 找不到 IC 时也会清掉 lastAuxText_,避免 FocusIn 时 重放旧状态(如"已插入"→切窗口→突然显示"已插入") 2. 300ms 延迟重试前检查 LAST_AUX,状态已变则跳过,避免旧文字覆盖 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit 7403597 |
1. set_aux_down/clear_aux_down 从 emit_capsule(被 cpal 音频回调调用) 移到独立线程执行,避免同步 DBus I/O 阻塞录音导致卡顿 2. is_plugin_installed_on_disk 同时检查 .so 和 .conf,孤立的 .so 没有 addon 配置 fcitx5 不会加载 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit c4cb3c9 |
1. start_dictation_signal_listener 新增 custom_trigger_key 参数, NameOwnerChanged 和初始同步都通过 resync_main_binding 分支处理 自定义组合键 vs 预设修饰键 2. ensure_plugin_installed 去掉"已安装即跳过"检查,AppImage 每次启动 都覆盖最新 .so + .conf,确保新 DBus 方法不会因旧插件丢失 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
b783115 to
163327e
Compare
|
Persistent review updated to latest commit 163327e |
1 similar comment
|
Persistent review updated to latest commit 163327e |
…ux")] 函数体内引用了 crate::linux_fcitx,非 Linux 平台该模块不存在导致 E0433。 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit 77455cf |
|
Persistent review updated to latest commit 1b0e41c |
|
在测试里我没发现相关问题 |
|
@claude 审查一下,然后告诉我结果。 |
|
Claude finished @appergb's task in 4m 3s —— View job 代码审查结果
总体评价整体设计合理,功能闭环完整。 问题 1 — 中等:NameOwnerChanged 回调内
|
| 改动 | 评价 |
|---|---|
bailian.rs sentence_id 去重 |
干净正确,用 BTreeMap<i64, String> 按 ID 覆盖完美解决累积重复 |
openless.cpp key.check() |
比手动比较 sym() + states() 更正确,handles modifiers 顺序差异 |
Shift 移除 filterAndAccept() |
正确修复了 Shift 被意外拦截导致 fcitx5 中英切换失效的问题 |
FocusIn 事件补发 auxDown |
设计合理;flushUI() 的防竞态处理也细心 |
| DBus I/O 挪离音频线程 | 正确,防止 cpal 录音回调因同步 DBus 调用而卡顿 |
LAST_AUX 防重复 DBus 调用 |
有效避免 ~30Hz 音频回调频繁触发 DBus 写 |
| CI bundle + 自动安装 | AppImage 闭环设计完整 |
Linux 胶囊窗口早退 (return) |
看到 emit_capsule 中 #[cfg(target_os = "linux")] { return; } 确认 FallbackShow 策略不会导致胶囊窗口意外弹出,逻辑正确 |
问题 1 建议合并前修复(会在 fcitx5 重启后的 1 秒窗口内丢弃所有热键信号),问题 2、3 可酌情处理。
1. NameOwnerChanged 回调内 sleep 移到独立线程,不再阻塞 DBus 事件循环 2. emit_capsule 首次 set_aux_down spawn 加 LAST_AUX 状态守卫 3. ensure_plugin_installed 注释更新为"总是覆盖"语义 4. NameOwnerChanged match rule 移除不支持的 arg0,改回调内过滤 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit 5208499 |
appergb
left a comment
There was a problem hiding this comment.
自动化代码审核 — PR #520
由 Agent 自动审核,最终合并决定由维护者把关。
| 严重度 | 数量 | 结论 |
|---|---|---|
| CRITICAL | 0 | pass |
| HIGH | 2 | 建议合并前修复 |
| MEDIUM | 3 | info |
| LOW | 3 | note |
[HIGH] _name_match 在 listener 真正运行前就被 drop — NameOwnerChanged 永远收不到
位置:openless-all/app/src-tauri/src/linux_fcitx.rs,start_dictation_signal_listener 内新增的 NameOwnerChanged 注册块。
let _name_match = match conn.add_match(fcitx_rule, move |...| { ... }) {
Ok(m) => m,
...
};
// 紧接着进入 retry 循环和 `loop {}` 事件循环dbus crate 的 add_match 返回一个 MsgMatch guard — guard drop 时 match 即被注销。_name_match 是局部变量,作用域在外层 block 结束时就被析构,而真正处理事件的 loop {} 还在后面。结果:fcitx5 重启后自动重新同步快捷键这条核心能力实际上不会工作,尽管源码看上去逻辑齐全。
修复:把 _name_match 的绑定挪到紧邻 loop {} 之前的位置,或者改用一个生命周期能贯穿整个事件循环的容器(例如 Vec<MsgMatch> 放在 thread-local 或外层作用域)。
[HIGH] 仓库里提交了一个空的 libopenless.so 占位文件
位置:openless-all/app/src-tauri/linux-fcitx5-plugin/libopenless.so(zero-byte,blob SHA e69de29b)
CI 在打包前会用真实编译产物覆盖这个路径,所以 release 流程是好的。但任何开发者在本地 clone 后跑 Linux build,如果没执行完整的 CI 插件构建步骤,ensure_plugin_installed 就会把这个空 .so 拷到 ~/.local/lib/fcitx5/,fcitx5 加载时静默失败,没有可用的错误信息。
建议二选一:
- 从源码树里删掉这个文件,仅依赖 CI 的
cp步骤;或 - 改成
.gitkeep哨兵 + 启动时检查文件大小,发现是空文件直接报错而不是拷过去。
[MEDIUM] emit_capsule Linux 分支:两次 spawn 的 retry thread 与 LAST_AUX 之间存在窄竞态
位置:coordinator.rs emit_capsule 内的 Linux 块。
was_none 触发时无条件 spawn 一个 300ms 后重试的线程。如果状态在 300ms 内 Recording → Idle → Recording,两个 retry 线程都会看到相同的 LAST_AUX 文本,都通过 current.as_deref() != Some(&text) 守卫,结果是 两次 DBus 调用。建议引入 AtomicBool cancel 标志,或者用单个 debounce task 替代每次都 spawn。
[MEDIUM] setAuxDown 中的 savedIc_ 存在窄析构窗口
位置:openless-all/scripts/linux-fcitx5-plugin/openless.cpp,setAuxDown / clearAuxDown。
注释说"事件处理线程和 DBus 处理线程都是 fcitx5 主事件循环,无竞态"——对从按键事件触发的路径是对的。但现在 setAuxDown 也会由 Rust 侧 std::thread::spawn 出来的线程经 DBus 触发,进入 fcitx5 event loop 后,理论上仍可能在 if (!ic) null-check 与 ic->inputPanel() 解引用之间,被同一 event loop 迭代中 dispatch 的 InputContextDestroyed 事件清空 savedIc_。flushUI() 能排空已有事件但挡不住其后到达的 destroy 事件。建议把 IC 访问 + panel 更新包进 instance_->callLater 或 foreachFocused 闭包里。
[MEDIUM] bailian.rs 去重:sentence_id == 0 的最终句会被静默丢弃
位置:openless-all/app/src-tauri/src/asr/bailian.rs 第 +69 行。
if is_sentence_final && sentence_id > 0 {
st.final_segments.insert(sentence_id, trimmed.to_string());
}sentence_id 通过 unwrap_or(0) 兜底。如果 API 偶尔返回 end_time > 0 但缺 sentence_id 的最终句,旧代码会 push 到 Vec,新代码会丢。至少应在 is_sentence_final && sentence_id == 0 时打一条 warn 日志。
[LOW] retry 循环阻塞了 listener 线程,听 NameOwnerChanged 与 30s 同步串行了
10 × 3s 同步等待结束之前 NameOwnerChanged 是不在跑的(即便修了上面的 drop bug 也存在 3s 窗口)。可接受,但和 HIGH#1 联动,建议先注册 match 再开始 retry。
[LOW] release-tauri.yml 旧注释
PR 已更新文案,无需处理。
[LOW] coordinator.rs 内联 use std::sync::Mutex;
#[cfg(target_os = "linux")] 块内的局部 use,与文件顶部 import 风格不一致,可选清理。
结论:WARNING — 2 个 HIGH 都直接影响 PR 自身宣称的功能(fcitx5 重启后自动同步 + AppImage 自动装插件),建议合并前修复。MEDIUM/LOW 不阻塞但值得跟进。
🤖 Generated by code-reviewer agent.
避免快速状态切换 (Done→Idle→Recording) 时旧 clear 跑到新 set 后面把新文字清掉。 NameOwnerChanged Stale Sync 是已知低概率场景,注释保留。 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Persistent review updated to latest commit de5c55b |
appergb
left a comment
There was a problem hiding this comment.
自动化复审 — PR #520(commits 5208499 + de5c55b 之后)
Follow-up review after author addressed my earlier review.
| 上次 HIGH 问题 | 当前状态 |
|---|---|
HIGH #1: _name_match 提前 drop |
✅ 已修复 |
HIGH #2: 空 libopenless.so 占位 |
❌ 仍未修复 |
| MEDIUM #2: setAuxDown / clearAuxDown IC 竞态 | ✅ 已修复 |
✅ HIGH #1 已修复
52084999 中 _name_match 的绑定已挪到外层作用域,生命周期覆盖了完整的 loop { conn.process(...) }。callback 内部增加了手工守卫 if name != "org.fcitx.Fcitx5" { return true; },替代了 dbus crate 不支持的 arg0 过滤。逻辑正确。
附加改进:thread::sleep 从 callback 内挪到 spawn 出来的线程里,顺便修了 reconnect 时事件循环被阻塞的潜在问题,赞。
❌ HIGH #2 仍未修复
openless-all/app/src-tauri/linux-fcitx5-plugin/libopenless.so 在当前 PR branch 中仍是 0 字节(wc -c 返回 0)。
ensure_plugin_installed 的注释更新为"启动时总会覆盖",但仓库里 bundle 进 AppImage 的 .so 本身还是空的。任何 Linux AppImage 构建仍会把零字节 .so 拷到 ~/.local/lib/fcitx5/,fcitx5 加载时静默失败。
建议二选一:
- 把这个文件从仓库里删掉,完全依赖 CI 在打包前编译并 cp 真实产物;或
- 启动时检查 .so 文件大小,0 字节直接报错而不是拷贝,避免误以为安装成功。
✅ MEDIUM #2 已修复
de5c55b 给 clear_aux_down 加了对称的状态守卫,在 spawn 出来的线程内重新检查 LAST_AUX.is_none(),与 set_aux_down 的守卫对齐。能正确防止陈旧的 clear 越过新的 set。
结论:HIGH #2 仍阻塞合并 — Linux AppImage 那条主线还是会出问题。
🤖 Generated by code-reviewer agent.
1. _name_match/_match 生命周期注释:Rust 的 `let _name =` 不会提前 drop,
仅有 `let _ =` 才会。两个 guard 与 loop{} 同闭包作用域,自动化审核 HIGH 误判。
2. emit_capsule retry 用 AtomicU64 gen counter 去重,避免快速状态切换时
多个 retry 线程同时生效。
3. bailian.rs sentence_id==0 的 final 句加 warn 日志,方便排查。
4. 删除空 libopenless.so 占位文件,CI 会在构建前实时 cp。
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
撤回 HIGH #1 —— 我看错了 Rust 语义@aeoform 抱歉,你 Rust 里 第二轮我的 reviewer 又"确认"了你"修复了"它 —— 实际上你只是加了注释解释这个误判,binding 一直是有效的。两次都错,丢人。 剩余情况:
HIGH #2 那条还是想确认下你的意图 —— 是不是想保持空文件作为 placeholder,或者准备让 CI 在打包前覆盖?如果是 CI 来覆盖,建议在 🤖 Generated by code-reviewer agent. |
|
Persistent review updated to latest commit 2832437 |
|
HIGH #2 仓库里 0 字节 libopenless.so修好了 |
HIGH #2 确认已修 — 我又误读了@aeoform 谢谢澄清。再仔细看了一次 当前 PR #520 状态:
从我这边看,PR #520 现在没有阻塞合并的问题了。 合并决定权留给维护者。 抱歉这次审核我自己出了两次错(HIGH #1 Rust 语义 + HIGH #2 误读 diff status),下次会更仔细。 🤖 Generated by code-reviewer agent. |
User description
概述
Linux 上通过 fcitx5 输入法候选框下方显示听写状态文字,替代 macOS/Windows 的胶囊窗口。同时修复 AppImage 不含 fcitx5 插件、热键启动时序等问题。
改动
fcitx5 插件 (
openless.cpp)SetAuxDown/ClearAuxDownDBus 接口InputContextFocusIn监听:切窗口自动跟随焦点flushUI先排空旧事件再设 auxDown,防止按键竞态覆盖热键启动容错 (
linux_fcitx.rs)start_dictation_signal_listener启动时等待 fcitx5 最多 30sNameOwnerChanged,fcitx5 重启后自动重新同步快捷键AppImage 插件自动安装
libopenless.so打入 AppImage 资源~/.local/lib/fcitx5/和~/.local/share/fcitx5/addon/files映射安装到系统路径其他
coordinator.rs/dictation.rs移除冗余调用,统一文案测试
🤖 Generated with Claude Code
PR Type
Bug fix, Enhancement, Tests
Description
Deduplicate DashScope final transcript segments.
Render Linux status through fcitx5 auxDown.
Auto-install bundled plugin in AppImage.
Resync hotkeys after fcitx5 restarts.
Diagram Walkthrough
File Walkthrough
2 files
Deduplicate final ASR sentence segmentsRemove duplicate polishing capsule emission4 files
Route Linux status and hotkey startupAuto-install bundled fcitx5 pluginAdd auxDown APIs and resync logicDisplay status text inside fcitx51 files
Bundle fcitx5 plugin into AppImage